using System;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Linq;
using HttpStatusCode = System.Net.HttpStatusCode;
using Microsoft.Samples.ServiceHosting.StorageClient;
using Log = Microsoft.Samples.ServiceHosting.HelloFabric.ApplicationEnvironment;

namespace BlogEngine.Core.Azure.BlogProvider
{
    public class DataProvider
    {
        private readonly StorageAccountInfo _account;
        private readonly TableStorage _tableStorage;

        private const int NumRetries = 3;
        private readonly RetryPolicy _tableRetry = RetryPolicies.RetryN(NumRetries, TimeSpan.FromSeconds(1));

        private readonly List<string> _createdTables = new List<string>();


        public DataProvider(bool tryInitialize)
            : this(tryInitialize, StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration())
        {
        }

        public DataProvider(bool tryInitialize, StorageAccountInfo account)
        {
            try
            {
                _account = account;
                _tableStorage = TableStorage.Create(_account);

                if (tryInitialize) // TODO: This doesn't work for fresh installs; probably because the AspProviders cannot be initialized
                    TryInitializeTables(); // TODO: This is quite slow and might cause the server to fail to start
            }
            catch (Exception e)
            {
                Log.LogError("Initialization of storage failed!");
                throw new Exception("Initialization of storage failed!", e);
            }
        }

        private void TryInitializeTables()
        {
            try
            {
                var settings = SelectAllRows<Tables.Setting>(); // TODO: Replace by a "select count(e)" for better perfs
                if (settings.Count > 0) // TODO: Better way to detect when needed
                    return;
            }
// ReSharper disable EmptyGeneralCatchClause
            catch { }
// ReSharper restore EmptyGeneralCatchClause

            Log.LogWarning("New installation. Initialize tables...");

            var appName = Microsoft.Samples.ServiceHosting.AspProviders.Configuration
                .GetStringValueWithGlobalDefault(new System.Collections.Specialized.NameValueCollection(),
                    "applicationName",
                    Microsoft.Samples.ServiceHosting.AspProviders.Configuration.DefaultProviderApplicationNameConfigurationString,
                    Microsoft.Samples.ServiceHosting.AspProviders.Configuration.DefaultProviderApplicationName, false);

            Setup.CreateTables(_account); // Note: This only works online
            Setup.FillTables(_account, appName);
        }

        private void TryCreateTable(TableStorage storage, string name) // TODO: Move to a Initialization module to run only once?
        {
            if (_createdTables.Contains(name))
                return;
            if (storage.DoesTableExist(name) || storage.TryCreateTable(name)) // Do not add to the list if TryCreateTable() fails!
                _createdTables.Add(name);
        }

        private TableStorageDataServiceContext CreateDataServiceContext(string tableName)
        {
            TryCreateTable(_tableStorage, tableName); // TODO: Probably no longer needed since we have Setup
            var svc = _tableStorage.GetDataServiceContext();
            svc.MergeOption = MergeOption.NoTracking;
            return svc;
        }

        private static string GetTableName<TRow>()
            where TRow : Tables.Row
        {
            return typeof(TRow).Name;
        }

        private static TRow Prepare<TRow>(string tableName, TRow row)
            where TRow : Tables.Row
        {
            row.PartitionKey = tableName;
            row.RowKey = row.GetRowKey();
            return row; // TODO: Convert all null properties to their default value
        }





        public TEntity SelectEntity<TEntity, TRow>(Guid id)
            where TEntity : new()
            where TRow : Tables.Row
        {
            return SelectEntity<TEntity, TRow>(Tables.Row.ToRowKey(id));
        }

        public TEntity SelectEntity<TEntity, TRow>(string rowKey)
            where TEntity : new()
            where TRow : Tables.Row
        {
            var row = SelectRow<TRow>(rowKey);
            return Convert<TEntity, TRow>(row);
        }

        public List<TEntity> SelectAllEntities<TEntity, TRow>()
            where TEntity : new()
            where TRow : Tables.Row
        {
            var rows = SelectAllRows<TRow>();
            return ConvertAll<TEntity, TRow>(rows);
        }

        public void InsertEntity<TEntity, TRow>(TEntity entity)
            where TRow : Tables.Row, new()
        {
            var row = Convert<TRow, TEntity>(entity);
            InsertRow(row);
        }

        public void UpdateEntity<TEntity, TRow>(TEntity entity)
            where TRow : Tables.Row, new()
        {
            var row = Convert<TRow, TEntity>(entity);
            UpdateRow(row);
        }

        public void DeleteEntity<TEntity, TRow>(TEntity entity)
            where TRow : Tables.Row, new()
        {
            var row = Convert<TRow, TEntity>(entity);
            DeleteRow(row);
        }


        public TRow SelectRow<TRow>(Guid id)
            where TRow : Tables.Row
        {
            return SelectRow<TRow>(Tables.Row.ToRowKey(id));
        }

        public TRow SelectRow<TRow>(string rowKey)
            where TRow : Tables.Row
        {
            var tableName = GetTableName<TRow>();
            try
            {
                var svc = CreateDataServiceContext(tableName);

                var query = from e in svc.CreateQuery<TRow>(tableName)
                            where e.RowKey == rowKey
                            select e;

                var q = new TableStorageDataServiceQuery<TRow>(query as DataServiceQuery<TRow>, _tableRetry);
                return q.ExecuteAllWithRetries().SingleOrDefault();
            }
            catch (Exception ex)
            {
                return Log.LogErrorAndThrowException<TRow>(ex, "Failed to process {0}#{1}.", tableName, rowKey);
            }
        }

        public List<TRow> SelectAllRows<TRow>()
            where TRow : Tables.Row
        {
            var tableName = GetTableName<TRow>();
            try
            {
                var svc = CreateDataServiceContext(tableName);

                var query = from e in svc.CreateQuery<TRow>(tableName)
                            select e;

                var q = new TableStorageDataServiceQuery<TRow>(query as DataServiceQuery<TRow>, _tableRetry);
                return q.ExecuteAllWithRetries().ToList();
            }
            catch (Exception ex)
            {
                return Log.LogErrorAndThrowException<List<TRow>>(ex, "Failed to process {0}s.", tableName);
            }
        }

        public void InsertRow<TRow>(TRow row)
            where TRow : Tables.Row
        {
            var tableName = GetTableName<TRow>();
            row = Prepare(tableName, row);
            try
            {
                var svc = CreateDataServiceContext(tableName);
                svc.AddObject(tableName, row);
                svc.SaveChanges();
            }
            catch (InvalidOperationException ex)
            {
                HttpStatusCode status;
                // when retry policies are used we cannot distinguish between a conflict and success
                // so, in the case of a conflict, we just retrun success here
                if (TableStorageHelpers.EvaluateException(ex, out status) && status == HttpStatusCode.Conflict)
                    return;

                Log.LogErrorAndThrowException(ex, "Failed to process {0}#{1}.", tableName, row.RowKey);
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}#{1}.", tableName, row.RowKey);
            }
        }

        public void InsertAllRows<TRow>(List<TRow> rows)
            where TRow : Tables.Row
        {
            if (rows.Count == 0)
                return; // Nothing to do
            var tableName = GetTableName<TRow>();
            try
            {
                var svc = CreateDataServiceContext(tableName);
                rows.ForEach(e => svc.AddObject(tableName, Prepare(tableName, e)));
                svc.SaveChanges();
            }
            catch (InvalidOperationException ex)
            {
                HttpStatusCode status;
                // when retry policies are used we cannot distinguish between a conflict and success
                // so, in the case of a conflict, we just retrun success here
                if (TableStorageHelpers.EvaluateException(ex, out status) && status == HttpStatusCode.Conflict)
                    return;

                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.Count={1}.", tableName, rows.Count);
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.Count={1}.", tableName, rows.Count);
            }
        }

        public void UpdateRow<TRow>(TRow row)
            where TRow : Tables.Row
        {
            var tableName = GetTableName<TRow>();
            row = Prepare(tableName, row);
            try
            {
                var svc = CreateDataServiceContext(tableName);
                svc.AttachTo(tableName, row, "*"); // TODO: This disables optimistic concurrency
                svc.UpdateObject(row);
                svc.SaveChanges();
            }
            catch (InvalidOperationException ex)
            {
                HttpStatusCode status;
                // when retry policies are used we cannot distinguish between a conflict and success
                // so, in the case of a conflict, we just retrun success here
                if (TableStorageHelpers.EvaluateException(ex, out status) && status == HttpStatusCode.Conflict)
                    return;

                Log.LogErrorAndThrowException(ex, "Failed to process {0}#{1}.", tableName, row.RowKey);
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}#{1}.", tableName, row.RowKey);
            }
        }

        public void InsertOrUpdateRow<TRow>(TRow row) // TODO: Should use the same persistence context
            where TRow : Tables.Row
        {
            row = Prepare(GetTableName<TRow>(), row);
            var persistedRow = SelectRow<TRow>(row.GetRowKey());
            if (persistedRow == null)
                InsertRow(row);
            else
            {
                persistedRow.CopyFrom(row);
                UpdateRow(persistedRow);
            }
        }

        public void DeleteRow<TRow>(TRow row)
            where TRow : Tables.Row
        {
            var tableName = GetTableName<TRow>();
            row = Prepare(tableName, row);
            try
            {
                var svc = CreateDataServiceContext(tableName);

                var query = from c in svc.CreateQuery<TRow>(tableName)
                            where c.RowKey == row.RowKey
                            select c;

                var q = new TableStorageDataServiceQuery<TRow>(query as DataServiceQuery<TRow>, _tableRetry);
                IEnumerable<TRow> rows = q.ExecuteAllWithRetries();

                int count = 0;
                foreach (TRow r in rows)
                {
                    if (count++ > 1)
                        Log.LogWarning("There are multiple rows with the same RowKey {0}#{1}. RowKey should be unique.", tableName, row.RowKey);

                    svc.AttachTo(tableName, r, "*"); // TODO: This disables optimistic concurrency
                    svc.DeleteObject(r);
                    svc.SaveChanges();
                }

                if (count == 0)
                    Log.LogWarning("There are no row to delete with the RowKey {0}#{1}.", tableName, row.RowKey);
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}#{1}.", tableName, row.RowKey);
            }
        }

        public void DeleteAllRows<TRow>()
            where TRow : Tables.Row
        {
            var tableName = GetTableName<TRow>();
            try
            {
                var svc = CreateDataServiceContext(tableName);

                var query = from e in svc.CreateQuery<TRow>(tableName)
                            select e;

                var q = new TableStorageDataServiceQuery<TRow>(query as DataServiceQuery<TRow>, _tableRetry);
                IEnumerable<TRow> rows = q.ExecuteAllWithRetries();

                foreach (TRow r in rows)
                {
                    svc.AttachTo(tableName, r, "*"); // TODO: This disables optimistic concurrency
                    svc.DeleteObject(r);
                }
                svc.SaveChanges();
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.", tableName);
            }
        }


        public List<TRow> SelectAllRelatedRows<TRow>(Tables.Post post)
            where TRow : Tables.PostRelatedRow
        {
            var tableName = GetTableName<TRow>();
            try
            {
                var svc = CreateDataServiceContext(tableName);

                var query = from e in svc.CreateQuery<TRow>(tableName)
                            where e.ParentId == post.Id
                            select e;

                var q = new TableStorageDataServiceQuery<TRow>(query as DataServiceQuery<TRow>, _tableRetry);
                return q.ExecuteAllWithRetries().ToList();
            }
            catch (Exception ex)
            {
                return Log.LogErrorAndThrowException<List<TRow>>(ex, "Failed to process {0}s.", tableName);
            }
        }

        public void InsertAllRelatedRows<TRow>(List<TRow> rows)
            where TRow : Tables.PostRelatedRow
        {
            InsertAllRows(rows);
        }

        public void DeleteAllRelatedRows<TRow>(Tables.Post post)
            where TRow : Tables.PostRelatedRow
        {
            var tableName = GetTableName<TRow>();
            try
            {
                var svc = CreateDataServiceContext(tableName);

                var query = from e in svc.CreateQuery<TRow>(tableName)
                            where e.ParentId == post.Id // TODO: Use the optimization in SelectAllRelatedRows()
                            select e;

                var q = new TableStorageDataServiceQuery<TRow>(query as DataServiceQuery<TRow>, _tableRetry);
                IEnumerable<TRow> rows = q.ExecuteAllWithRetries();

                foreach (TRow r in rows)
                {
                    svc.AttachTo(tableName, r, "*"); // TODO: This disables optimistic concurrency
                    svc.DeleteObject(r);
                }
                svc.SaveChanges();
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}s related to post {1}.", tableName, post.Id);
            }
        }


        public TOutput Convert<TOutput, TInput>(TInput input)
            where TOutput : new()
        {
            return Tables.Row.Convert<TOutput, TInput>(input);
        }

        public List<TOutput> ConvertAll<TOutput, TInput>(List<TInput> inputs)
            where TOutput : new()
        {
            var outputs = new List<TOutput>(inputs.Count);
            foreach (var input in inputs)
                outputs.Add(Convert<TOutput, TInput>(input));
            return outputs;
        }
    }
}